跳到主要内容

Redis 各种数据结构的使用场景

String 的使用场景

String 的内部编码

Redis会根据当前值的类型和长度决定使用哪种内部编码来实现。

字符串类型的内部编码有3种:

  1. int:8 个字节的长整型。
  2. embstr:小于等于 39 个字节的字符串。
  3. raw:大于 39 个字节的字符串。

数据库缓存

在 web 服务中,使用 MySQL 作为数据库,Redis作为缓存。

由于 Redis 具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web 端的大多数请求都是从 Redis 中获取的数据,如果 Redis 中没有需要的数据,则会从 MySQL 中去获取,并将获取到的数据写入 Redis。

阅读量,播放量计数

Redis 中有一个字符串相关的命令 incr key,incr 命令对值做自增操作,返回结果分为以下三种情况:

  • 值不是整数,返回错误
  • 值是整数,返回自增后的结果
  • key 不存在,默认键为 0,返回 1(就是直接新键一个 key)

比如文章的阅读量,视频的播放量等等都会使用 Redis 来计数,每播放一次,对应的播放量就会加 1,同时将这些数据异步存储到数据库中达到持久化的目的。

共享 Session

在分布式系统中,用户的每次请求会访问到不同的服务器,这就会导致 Session 不同步的问题,假如一个用来获取用户信息的请求落在A 服务器上,获取到用户信息后存入 Session。

下一个请求落在 B 服务器上,想要从 Session 中获取用户信息就不能正常获取了,因为用户信息的 Session 在服务器A上,为了解决这个问题,使用 Redis 集中管理这些 Session,将 Session 存入 Redis,使用的时候直接从 Redis中获取就可以了。

IP 限速

为了安全考虑,有些网站会对 IP 进行限制,限制同一 IP 在一定时间内访问次数不能超过 n 次。

自定义注解 + Redis实现简单 IP 限流

哈希 Hash 的使用场景

Redis 中,哈希类型是指一个键值对的存储结构。

# 储存语法
hset key field value

# Redis 的 Hash 可以同时插入多个数据
hset key field01 value01 field02 value02 field03 value03

# 获取
hget key field

# 获取所有列
hgetall key

# 删除
hdel key field

维护一个简单的数据表

由于 hash 类型存储的是一个键值对,比如数据库有以下一个用户表结构

idnameage

将以上信息存入 redis,用表明 user:id 作为 key,用户属性作为值:

# hset 可以同时插入多个字段
# 语法:
hset key field01 value01 field02 value02 field03 value03

# 使用如下(注意,这里的 user:1 整个为一个 key):
hset user:1 name 张三 age 18

使用哈希存储会比字符串更加方便直观

这种数据表只适合索引查询,不适合范围查询(Hash 也没这功能)

装载聚合对象

Hash 可以用于表示对象的属性,每个字段表示对象的一个属性,值表示该属性的值。

像一些聚合对象可以丢到 Redis 中,通过 Hash 来存储,这样可以很方便的只取得对象某些信息,而不用取出整个对象。(巨无霸聚合根)

列表 List 使用场景

列表类型用来存储多个有序的字符串,一个列表最多可以存储 2^32-1 个元素,列表的两端都可以插入和弹出元素。

List 的内部编码

列表的内部编码有两种:

ziplist(压缩列表):当哈希类型元素个数小于 list-max-ziplist-entries 配置(默认512个)同时所有值都小于 list-max-ziplist-value 配置(默认64字节)时使用。这个 ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以比 hashtable 更加节省内存。

linkedlist(链表):当 ziplist 不能满足要求时,会使用 linkedlist。

消息队列

列表用来存储多个有序的字符串,既然是有序的,那么就满足消息队列的特点。使用 lpush + rpop 或者 rpush + lpop 实现消息队列。

除此之外,Redis 支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列(主要就是使用阻塞队列)。

BLPOP 是阻塞式列表的弹出原语。 它是命令 LPOP 的阻塞版本,这是因为当给定列表内没有任何元素可供弹出的时候, 连接将被 BLPOP 命令阻塞。

直到有另一个客户端对给定的这些 key 的任意一个执行 LPUSH 或 RPUSH 命令为止。 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素

返回值:返回 key 和弹出的元素值

超时时返回 nil

有时候,为了等待一个新元素到达数据中,需要使用轮询的方式对数据进行探查。

另一种更好的方式是,使用系统提供的阻塞原语,在新元素到达时立即进行处理,而新元素还没到达时,就一直阻塞住,避免轮询占用资源。BLPOP 和 BRPOP 能很好解决这个问题。

和 Java 提供的阻塞队列如 LinkedBlockingQueue 相比,Redis 提供读取阻塞,不提供写入阻塞。

在 Spring 中使用 Redis,实现线程阻塞

//list集合 第一个元素为key值,第二个元素为弹出的元素值;当超时返回[null]
List<Object> obj = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Nullable
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
//队列没有元素会阻塞操作,直到队列获取新的元素或超时
return connection.bLPop(timeout, key.getBytes());
}
}, new StringRedisSerializer());

我们通过向队列添加元素来释放阻塞的线程

redisTemplate.opsForList().rightPush(key,value);

维护一个栈

由于列表存储的是有序字符串,满足队列的特点,也就能满足栈先进后出的特点,使用 lpush + lpop 或者 rpush + rpop 实现栈。

分页获取文章列表

因为列表的元素不但是有序的,而且还支持按照索引范围获取元素。因此我们可以使用命令 lrange key 0 9 分页获取文章列表

集合 Set 的使用场景

集合类型也可以保存多个字符串元素,与列表不同的是,集合中不允许有重复元素并且集合中的元素是无序的。一个集合最多可以存储 2^32-1 个元素。

Set 的内部编码

集合类型的内部编码有两种:

intset(整数集合):当集合中的元素都是整数且元素个数小于 set-max-intset-entries 配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。

hashtable(哈希表):当 intset 不能满足要求时,会使用 hashtable。

用户标签 Tag(交并集)

例如一个用户对篮球、足球感兴趣,另一个用户对橄榄球、乒乓球感兴趣,这些兴趣点就是一个标签。

有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同感兴趣的标签。

给用户打标签的时候需要:

  1. 给用户打标签,
  2. 给标签加用户。

注意:需要给这两个操作增加事务

给用户打标签

sadd user:1:tags tag1 tag2

给标签添加用户

sadd tag1:users user:1

sadd tag2:users user:1

使用交集(sinter)求两个 user 的共同标签

sinter user:1:tags user:2:tags

抽奖功能(随机取列表)

集合有两个命令支持获取随机数,分别是:

随机获取 count 个元素,集合元素个数不变

srandmember key [count]

随机弹出 count 个元素,元素从集合弹出,集合元素个数改变

spop key [count]

用户点击抽奖按钮,参数抽奖,将用户编号放入集合,然后抽奖,分别抽一等奖、二等奖,如果已经抽中一等奖的用户不能参数抽二等奖则使用 spop,反之使用 srandmember。

有序集合 ZSet(Sorted Set)

有序集合和集合一样,不能有重复元素。但是可以排序,它给每个元素设置一个 Score 作为排序的依据。最多可以存储 2^32-1 个元素。

它还可以用于取交集、并集、差集(和上面的 Set 集合是一样的)。

ZSet 内部编码

有序集合类型的内部编码有两种:

1、ziplist(压缩列表):当有序集合的元素个数小于list-max-ziplist-entries配置(默认128个)同时所有值都小于list-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,更加节省内存。

2、skiplist(跳跃表):当不满足 ziplist 的要求时,会使用 skiplist。

具体数据结构可以看 “Redis 各种数据结构的内部原理” 那篇文章。

排行榜

用户发布了 n 篇文章,其他人看到文章后给喜欢的文章点赞,使用 score 来记录点赞数,有序集合会根据 score 排行。(具体细节看 Redis 实战那篇文章)

流程如下:

用户发布一篇文章,初始点赞数为 0,即 score 为 0

zadd user:article 0 a

有人给文章 a 点赞,递增 1

zincrby user:article 1 a

查询点赞前三篇文章

zrevrange user:article 0 2

查询点赞后三篇文章

zrange user:article 0 2

延迟消息队列

下单系统,下单后需要在 15 分钟内进行支付,如果 15 分钟未支付则自动取消订单。

将下单后的十五分钟后时间作为 score,订单作为 value 存入 redis,消费者轮询去消费,如果消费的大于等于这笔记录的 score,则将这笔记录移除队列,取消订单。

查询价格的范围

ZSET 提供了根据分数范围进行查询的功能。例如,可以通过指定最小和最大分数来获取某个分数范围内的成员列表,这对于实现区间查询非常有用。

假设我们有一个有序集合,存储了不同商品的价格信息,并且每个商品的价格作为有序集合的分数。我们想要查询价格在某个范围内的商品列表。

示例命令如下:

ZADD products 10 "Product A"
ZADD products 15 "Product B"
ZADD products 20 "Product C"
ZADD products 25 "Product D"
ZADD products 30 "Product E"

ZRANGEBYSCORE products 15 25

以上命令执行了以下操作:

  1. 使用 ZADD 命令向有序集合 products 中添加商品和对应的价格。每个商品的价格作为有序集合的分数。
  2. 使用 ZRANGEBYSCORE 命令查询有序集合 products 中价格在 15 到 25 之间的商品列表。参数15和25分别表示范围的最小值和最大值。

执行以上命令后,ZRANGEBYSCORE 命令将返回价格在 15 到 25 之间的商品列表,按照价格从低到高排序。你可以根据需要进行后续处理或展示查询结果。

需要注意的是,ZRANGEBYSCORE 命令还提供了一些可选参数,例如指定返回结果的数量、限定结果的偏移量等,以满足不同的查询需求。

事件调度和过期处理

ZSET 的分数可以表示时间戳或时间窗口,可以使用 ZSET 来管理和调度基于时间的事件。通过添加事件的时间戳作为分数,可以使用 ZSET 的有序性质获取过期事件或按时间顺序执行事件。

假设我们有一个有序集合,存储了一些事件的时间戳,并且每个事件的时间戳作为有序集合的分数。我们希望使用ZSET来进行事件调度和过期处理,即根据事件的时间戳来获取过期事件或按时间顺序执行事件。

示例命令如下:

ZADD events 1622880000 "Event A"
ZADD events 1622890000 "Event B"
ZADD events 1622900000 "Event C"
ZADD events 1622910000 "Event D"
ZADD events 1622920000 "Event E"

ZREMRANGEBYSCORE events 0 1622900000
ZRANGE events 0 -1

以上命令执行了以下操作:

  1. 使用 ZADD 命令向有序集合 events 中添加事件和对应的时间戳。每个事件的时间戳作为有序集合的分数。
  2. 使用 ZREMRANGEBYSCORE 命令删除时间戳在 0 到 1622900000 之间的事件,即删除过期的事件。参数0和1622900000分别表示范围的最小值和最大值。
  3. 使用 ZRANGE 命令获取有序集合 events 中的所有事件,通过指定范围 0 -1 获取全部事件。

执行以上命令后,ZREMRANGEBYSCORE 命令将删除过期的事件,而 ZRANGE 命令将返回剩余的未过期事件列表,按照时间戳从低到高排序。你可以根据需要进行后续处理或执行相应的操作。

基数统计 HyperLogLog

HyperLogLog 数据结构在以下场景中非常有用:

  1. 唯一用户统计:HyperLogLog 可以用于估算独立用户的数量,例如网站的访问者数量、应用程序的用户数量等。通过将用户的标识作为元素添加到 HyperLogLog 中,然后使用 PFCOUNT 命令获取基数估算值,可以快速估算独立用户的数量,而不需要存储和维护每个用户的具体信息。

  2. 页面点击量统计:HyperLogLog 可以用于估算页面点击量(Pageviews)的数量。通过将用户的访问标识(如 IP 地址或用户 ID)作为元素添加到 HyperLogLog 中,然后使用 PFCOUNT 命令获取基数估算值,可以得到页面的独立访问数量。

  3. 社交媒体分享统计:HyperLogLog 可以用于估算某个内容在社交媒体上的分享次数。通过将用户的分享标识(如分享 ID)作为元素添加到 HyperLogLog 中,然后使用 PFCOUNT 命令获取基数估算值,可以快速估算内容的独立分享次数。

  4. 广告点击量统计:HyperLogLog 可以用于估算广告的点击次数。通过将用户的点击标识(如点击 ID)作为元素添加到 HyperLogLog 中,然后使用 PFCOUNT 命令获取基数估算值,可以快速估算广告的独立点击次数。

需要注意的是,HyperLogLog 是一种概率性数据结构,估算的基数值是近似值,并且可能存在一定的误差。误差的范围通常受到位图大小和元素数量的影响。在实际使用中,可以根据需求设置合适的位图大小以平衡内存占用和估算精度。

当使用 HyperLogLog 进行基数统计时,以下是一个具体的使用场景例子,包含对应的 Redis 命令:

估算每天独立用户的数量

假设你有一个在线论坛,你想要估算每天独立用户的数量。你可以使用 HyperLogLog 数据结构来进行基数统计。

  1. 用户登录:

    每当用户登录到论坛时,你可以使用 PFADD 命令将用户的标识添加到 HyperLogLog 中。例如:

    PFADD daily_users_set user1
    PFADD daily_users_set user2
    PFADD daily_users_set user3

    这将将用户标识 "user1"、"user2" 和 "user3" 添加到名为 "daily_users_set" 的 HyperLogLog 中。

  2. 获取基数估算值:

    使用 PFCOUNT 命令可以获取 HyperLogLog 中的基数估算值,即独立用户的数量。例如:

    PFCOUNT daily_users_set

    这将返回一个近似的用户数量。

  3. 重置 HyperLogLog:

    每天结束后,你可能希望重置 HyperLogLog,以便进行下一天的统计。使用 PFDEL 命令可以删除 HyperLogLog。例如:

    PFDEL daily_users_set

    这将删除名为 "daily_users_set" 的 HyperLogLog。

通过上述流程,你可以每天使用 HyperLogLog 进行独立用户数量的估算。请注意,基于 HyperLogLog 的基数估算是近似值,但通常能够提供相对准确的结果,同时在占用较小内存的情况下进行高效计数。

哪些结构时间复杂度较高

Redis 是一个高效的内存键值存储数据库,它通过优化的数据结构和算法来实现高性能。大多数 Redis 操作的时间复杂度都是 O(1)(常数时间复杂度),这意味着无论数据规模的大小,操作所需的时间都是固定的。

然而,有一些特殊的 Redis 数据结构或操作可能具有较高的时间复杂度,取决于具体的使用情况。以下是一些在特定情况下可能具有较高时间复杂度的 Redis 结构和操作:

  1. 集合(Set)和有序集合(Sorted Set)的插入和删除操作:对于较大的集合和有序集合,插入和删除操作的时间复杂度可能会随着数据规模的增加而增加,最坏情况下可以达到 O(N),其中 N 是集合或有序集合中的元素数量。

  2. 集合(Set)和有序集合(Sorted Set)的交集、并集、差集操作:当进行多个集合或有序集合之间的交集、并集或差集操作时,操作的时间复杂度将取决于参与操作的集合的大小,最坏情况下可能达到 O(N*M),其中 N 和 M 分别是参与操作的集合的大小。

  3. 列表(List)的插入和删除操作:当在列表的两端进行插入和删除操作时,时间复杂度是 O(1)。但如果在列表的中间位置进行插入和删除操作,将需要移动和调整元素的位置,时间复杂度将变为 O(N),其中 N 是列表的长度。

需要根据具体的使用场景和操作需求来评估数据结构的时间复杂度,并根据性能要求进行适当的优化和调整。大多数情况下,Redis 的操作时间复杂度仍然非常高效,并能够满足大部分的实际需求。